package world.geometry;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;

import javax.media.j3d.Appearance;
import javax.media.j3d.ColoringAttributes;
import javax.media.j3d.Geometry;
import javax.media.j3d.Material;
import javax.media.j3d.PolygonAttributes;
import javax.media.j3d.QuadArray;
import javax.media.j3d.Shape3D;
import javax.media.j3d.Transform3D;
import javax.media.j3d.TransformGroup;
import javax.media.j3d.TriangleFanArray;
import javax.vecmath.Vector3d;

public class Clock extends TransformGroup {

	private boolean DEBUG = false;
	
    private Shape3D sides_;
    private Shape3D front_;
    private Shape3D back_;
    private Shape3D hour_hand_;
    private Shape3D minute_hand_;

    private TransformGroup tgHourHand_;
    private TransformGroup tgMinuteHand_;

    private int hour_;
    private int minute_;
    private int second_;

    private double vmin_ = -1.0;
    private double vmax_ = 1.0;
    private double delta_u_ = 0.1;
    private double delta_v_ = 0.1;

    public Clock () {

	sides_ = new Shape3D ();
	sides_.setGeometry (sidesGeometry_ ());
	sides_.setAppearance (myAppearance_ ());
	front_ = new Shape3D ();
	front_.setGeometry (frontGeometry_ (vmax_));
        front_.setAppearance (myAppearance_ ());
	back_ = new Shape3D ();
	back_.setGeometry (frontGeometry_ (vmin_));
        back_.setAppearance (myAppearance_ ());
        hour_hand_ = new Shape3D ();
        hour_hand_.setGeometry (hourHandGeometry_ ());
        hour_hand_.setAppearance (handAppearance_ ());
        minute_hand_ = new Shape3D ();
        minute_hand_.setGeometry (minuteHandGeometry_ ());
        minute_hand_.setAppearance (handAppearance_ ());
	addChild (sides_);
	addChild (front_);
	addChild (back_);
	tgHourHand_ = new TransformGroup ();
        tgMinuteHand_ = new TransformGroup ();
	tgHourHand_.setCapability (TransformGroup.ALLOW_TRANSFORM_WRITE);
	tgMinuteHand_.setCapability (TransformGroup.ALLOW_TRANSFORM_WRITE);
        tgHourHand_.addChild (hour_hand_);
        tgMinuteHand_.addChild (minute_hand_);
	addChild (tgHourHand_);
	addChild (tgMinuteHand_);
	Transform3D transform = new Transform3D ();
	transform.setScale (0.3);
	setTransform (transform);
	setTimeNow ();
    }

    public void setTime (int hour, int minute, int second) {

	hour_ = hour;
        minute_ = minute;
	second_ = second;
	update_ ();
    }

    public void setTimeNow () {

	Calendar cal = new GregorianCalendar ();
	cal.setTime (new Date ());
	int hour = cal.get (Calendar.HOUR);
	int minute = cal.get (Calendar.MINUTE);
	if (hour != hour_ || minute != minute_) {

	    hour_ = hour;
	    minute_ = minute;
	    update_ ();
	}
    }

    private void update_ () {

	Transform3D tfHourHand = new Transform3D ();
        Transform3D tfMinuteHand = new Transform3D ();

	// These translate the clock hands to the correct location
	// on the clock
	Vector3d hourHandOffset = new Vector3d (0.0, 0.75, 0.0);
	Vector3d minuteHandOffset = new Vector3d (0.0, 0.75, 0.0);

	// These translate the clock hands to the correct location
	// for rotation
	Vector3d hourHandRotOffset = new Vector3d (0.0, 0.6/3.5, 0.0);
	Vector3d minuteHandRotOffset = new Vector3d (0.0, 1.0/3.5, 0.0);

	Transform3D hourRotation = new Transform3D ();
	Transform3D minuteRotation = new Transform3D ();

	double minuteAngle = minute_/60.0 * 2.0 * Math.PI;
	double hourAngle = (hour_/12.0 * 2.0 * Math.PI) + (minuteAngle/12.0);

        hourRotation.rotZ (-hourAngle);
        minuteRotation.rotZ (-minuteAngle);

        hourRotation.setTranslation (hourHandOffset);
        minuteRotation.setTranslation (minuteHandOffset);

	tfHourHand.mul (hourRotation);
	tfMinuteHand.mul (minuteRotation);

	tgHourHand_.setTransform (tfHourHand);
        tgMinuteHand_.setTransform (tfMinuteHand);
    }

    private Geometry sidesGeometry_ () {

	double curvePoints[] = calc_curve_points_ (delta_u_);
        double patchPoints[] = extrude_solid_ (curvePoints);

	int cols = curvePoints.length / 3;
	int rows = (int)((vmax_ - vmin_) / delta_v_) + 1;
	int num_faces = (rows - 1) * (cols - 1);
	int num_verts = num_faces * 4;

	QuadArray result = new QuadArray (num_verts,
                                  QuadArray.COORDINATES | QuadArray.NORMALS);
	double quadFaces[] = calc_faces_ (patchPoints, rows, cols);
	float quadNormals[] = calc_normals_ (quadFaces, rows, cols);
	result.setCoordinates(0, quadFaces);
	result.setNormals(0, quadNormals);
	return result;
    }

    private Geometry frontGeometry_ (double z) {

	double curvePoints[] = calc_curve_points_ (delta_u_);
	int num_verts = curvePoints.length / 3;
        int[] stripVertexCounts = new int[1];
	stripVertexCounts[0] = num_verts;

        float normals[] = new float[curvePoints.length];
        for (int i = 0; i < num_verts; i++) {

	    normals[3*i] = 0.0f;
	    normals[3*i+1] = 0.0f;
	    normals[3*i+2] = 1.0f;

	    curvePoints[3*i+2] = z;
	}

	TriangleFanArray result = new TriangleFanArray (num_verts,
                      TriangleFanArray.COORDINATES | TriangleFanArray.NORMALS,
		      stripVertexCounts);
	result.setCoordinates(0, curvePoints);
	result.setNormals(0, normals);
	return result;
    }

    private Geometry minuteHandGeometry_ () {

	QuadArray result = new QuadArray (4,
                                  QuadArray.COORDINATES | QuadArray.NORMALS);
	double verts[] = new double[12];
	float normals[] = new float[12];
	double z = vmax_ + 0.05;
	verts[0] = -0.1/3.5; verts[1] = 1.0/3.5; verts[2] = z;
	verts[3] = 0.0; verts[4] = 0.0; verts[5] = z;
	verts[6] = 0.1/3.5; verts[7] = 1.0/3.5; verts[8] = z;
	verts[9] = 0.0; verts[10] = 2.0/3.5; verts[11] = z;

	for (int i = 0; i < 4; i++) {

	    normals[3*i] = 0.0f;
	    normals[3*i+1] = 0.0f;
	    normals[3*i+2] = 1.0f;
  	}
	result.setCoordinates(0, verts);
	result.setNormals(0, normals);
	return result;
    }

    
    private Geometry hourHandGeometry_ () {

	QuadArray result = new QuadArray (4,
                                  QuadArray.COORDINATES | QuadArray.NORMALS);
	double verts[] = new double[12];
	float normals[] = new float[12];
	double z = vmax_ + 0.05;
	verts[0] = -0.15/3.5; verts[1] = 0.6/3.5; verts[2] = z;
	verts[3] = 0.0; verts[4] = 0.0; verts[5] = z;
	verts[6] = 0.15/3.5; verts[7] = 0.6/3.5; verts[8] = z;
	verts[9] = 0.0; verts[10] = 1.2/3.5; verts[11] = z;

	for (int i = 0; i < 4; i++) {

	    normals[3*i] = 0.0f;
	    normals[3*i+1] = 0.0f;
	    normals[3*i+2] = 1.0f;
  	}
	result.setCoordinates(0, verts);
	result.setNormals(0, normals);
	return result;
    }

    
    private Appearance myAppearance_ () {

	Appearance result = new Appearance ();
	Material material = new Material ();
	material.setEmissiveColor (0.1f, 0.1f, 0.9f);
	material.setShininess (128.0f);
	result.setMaterial (material);
	ColoringAttributes ca = result.getColoringAttributes ();
	if (ca == null) {
	
	    ca = new ColoringAttributes ();
        }
        ca.setShadeModel (ColoringAttributes.SHADE_GOURAUD);
	result.setColoringAttributes (ca);
        PolygonAttributes pa = result.getPolygonAttributes ();
	if (pa == null) {

	    pa = new PolygonAttributes ();
	}
	pa.setCullFace (PolygonAttributes.CULL_NONE);
	result.setPolygonAttributes (pa);
	return result;
    }

    private Appearance handAppearance_ () {

	Appearance result = new Appearance ();
	Material material = new Material ();
	material.setAmbientColor (0f, 0f, 0f);
	material.setDiffuseColor (0f, 0f, 0f);
	result.setMaterial (material);
        PolygonAttributes pa = result.getPolygonAttributes ();
	if (pa == null) {

	    pa = new PolygonAttributes ();
	}
	pa.setCullFace (PolygonAttributes.CULL_NONE);
	result.setPolygonAttributes (pa);
	return result;
    }

    private double[] calc_curve_points_ (double delta) {

	double umin = -1.5;
	double umax = 1.5;
 	double curve_start = -1.0;
	double curve_end = 1.0;
	double base_height = 0.5;

	int num_points = (int)((umax - umin) / delta) + 5;
	double[] points = new double[3*num_points];

	double x;
	double y;

	for (int i = 2; i < num_points-2; i++) {

	    x = umin + (double)(i-2) * delta;
	    if (x >= curve_start && x <= curve_end) {

	        y = (-1.0 * x * x + 1.0) + base_height;
	    }
	    else {

		y = base_height;
	    }

	    points[3*i] = x;
	    points[3*i+1] = y;
	    points[3*i+2] = 0.0;
	}
	points[3] = umin;
	points[4] = 0.0;
	points[5] = 0.0;

	int idx = (num_points-2) * 3;
        points[idx] = umax;
        points[idx+1] = 0.0;
        points[idx+2] = 0.0;

	idx = (num_points-1) * 3;
	points[idx] = points[0] = 0.0;
	points[idx+1] = points[1] = 0.0;
	points[idx+2] = points[2] = 0.0;

	return points;
    }

    private double[] extrude_solid_ (double[] line) {

	int row_points = line.length / 3;
	int col_points = (int)((vmax_ - vmin_) / delta_v_) + 1;
	double patch[] = new double[row_points * col_points * 3];

	int base_idx;
	double z;
	double z_max = -999.0;
	double z_min = 999.0;
	for (int v = 0; v < col_points; v++) {

	    z = delta_v_ * v + vmin_;
	    for (int u = 0; u < row_points; u++) {

	        base_idx = (row_points*v + u) * 3;
	        patch[base_idx] = line[u*3];
	        patch[base_idx + 1] = line[u*3+1];
	        patch[base_idx + 2] = z;
	    }
	}
	return patch;
    }

    private void printArray_ (double[] array) {

	for (int i = 0; i < array.length; i++) {

	    System.out.print (array[i] + "  ");
	    if (((i +1) % 3) == 0)
		System.out.println ();
	}
    }

    private double[] calc_faces_ (
	double[] patch,
	int num_rows,
	int num_cols) {

	int size = (num_rows - 1) * (num_cols - 1) * 12;
	double[] result = new double[size];
        int face_number;
	int face_start_idx;
	int base_idx;

	for (int v = 0; v < num_rows - 1; v++) {  // row major

	    for (int u = 0; u < num_cols - 1; u++) { 

	        face_number = v * (num_cols - 1) + u;
		face_start_idx = face_number * 12;

	        // top left vertex for this face
	        base_idx = (v * num_cols + u) * 3;
	        result[face_start_idx] = patch[base_idx];
	        result[face_start_idx+1] = patch[base_idx+1];
	        result[face_start_idx+2] = patch[base_idx+2];

	        // bottom left vertex for this face
	        base_idx = ((v+1) * num_cols + u) * 3;
	        result[face_start_idx+3] = patch[base_idx];
	        result[face_start_idx+4] = patch[base_idx+1];
	        result[face_start_idx+5] = patch[base_idx+2];

	        // bottom right vertex for this face
	        base_idx = ((v+1) * num_cols + u+1) * 3;
	        result[face_start_idx+6] = patch[base_idx];
	        result[face_start_idx+7] = patch[base_idx+1];
	        result[face_start_idx+8] = patch[base_idx+2];

	        // top right vertex for this face
	        base_idx = (v * num_cols + u+1) * 3;
	        result[face_start_idx+9] = patch[base_idx];
	        result[face_start_idx+10] = patch[base_idx+1];
	        result[face_start_idx+11] = patch[base_idx+2];
  	    }
        }
	return result;
    }

    private float[] calc_normals_ (
	double[] faces,
	int num_rows,
	int num_cols) {

	int size = (num_rows - 1) * (num_cols - 1) * 12;
	float[] result = new float[size];
	float[] face_normals = new float[size/4];

	int face_idx;
	Vector3d v1 = new Vector3d ();
	Vector3d v2 = new Vector3d ();
	Vector3d normal = new Vector3d ();

	// pass 1 - calculate the normal for each face
	for (int v = 0; v < num_rows - 1; v++) {

	    for (int u = 0; u < num_cols - 1; u++) {

		face_idx = (v * (num_cols-1) + u) * 12;

		// v1 goes from top left vertex to top right vertex
		v1.x = faces[face_idx+9] - faces[face_idx];
		v1.y = faces[face_idx+10] - faces[face_idx+1];
		v1.z = faces[face_idx+11] - faces[face_idx+2];

		// v2 goes from top left vertex to bottom left vertex
		v2.x = faces[face_idx+3] - faces[face_idx];
		v2.y = faces[face_idx+4] - faces[face_idx+1];
		v2.z = faces[face_idx+5] - faces[face_idx+2];

	        normal.cross (v2, v1);
	  	normal.normalize ();

		face_idx /= 4;
		face_normals[face_idx] = (float)normal.x;
		face_normals[face_idx+1] = (float)normal.y;
		face_normals[face_idx+2] = (float)normal.z;
            }
        }

	// pass 2 - calculate vertex normals as averages of
	// the normals of adjacent faces for interior (ie
	// non-boundary) vertices.
	int tr_face_idx;
	int tl_face_idx;
	int br_face_idx;
	int bl_face_idx;
        int top;
	int bottom;
        int left;
	int right;
	for (int v = 0; v < num_rows - 1; v++) {

	    for (int u = 0; u < num_cols - 1; u++) {

		// top left 
	        top = (v==0)? v : v-1;
		bottom = v;
		left = (u==0)? u : u-1;
		right = u;
	        tl_face_idx = (top * (num_cols-1) + left) * 3;
	        tr_face_idx = (top * (num_cols-1) + right) * 3;
	        bl_face_idx = (bottom * (num_cols-1) + left) * 3;
	        br_face_idx = (bottom * (num_cols-1) + right) * 3;

		normal.x = face_normals[tl_face_idx] +
			   face_normals[tr_face_idx] +
			   face_normals[bl_face_idx] +
			   face_normals[br_face_idx];

		normal.y = face_normals[tl_face_idx+1] +
			   face_normals[tr_face_idx+1] +
			   face_normals[bl_face_idx+1] +
			   face_normals[br_face_idx+1];

		normal.z = face_normals[tl_face_idx+2] +
			   face_normals[tr_face_idx+2] +
			   face_normals[bl_face_idx+2] +
			   face_normals[br_face_idx+2];

		normal.normalize ();

		face_idx = (v * (num_cols-1) + u) * 12;
		result[face_idx] = (float)normal.x;
		result[face_idx+1] = (float)normal.y;
		result[face_idx+2] = (float)normal.z;

		// bottom left
	        top = v;
		bottom = (v>=num_rows-2)? v : v+1;
		left = (u==0)? u : u-1;
		right = u;
	        tl_face_idx = (top * (num_cols-1) + left) * 3;
	        tr_face_idx = (top * (num_cols-1) + right) * 3;
	        bl_face_idx = (bottom * (num_cols-1) + left) * 3;
	        br_face_idx = (bottom * (num_cols-1) + right) * 3;

		normal.x = face_normals[tl_face_idx] +
			   face_normals[tr_face_idx] +
			   face_normals[bl_face_idx] +
			   face_normals[br_face_idx];

		normal.y = face_normals[tl_face_idx+1] +
			   face_normals[tr_face_idx+1] +
			   face_normals[bl_face_idx+1] +
			   face_normals[br_face_idx+1];

		normal.z = face_normals[tl_face_idx+2] +
			   face_normals[tr_face_idx+2] +
			   face_normals[bl_face_idx+2] +
			   face_normals[br_face_idx+2];

		normal.normalize ();

		result[face_idx+3] = (float)normal.x;
		result[face_idx+4] = (float)normal.y;
		result[face_idx+5] = (float)normal.z;

		// bottom right 
	        top = v;
		bottom = (v>=num_rows-2)? v : v+1;
		left = u;
		right = (u>=num_cols-2)? u : u+1;
	        tl_face_idx = (top * (num_cols-1) + left) * 3;
	        tr_face_idx = (top * (num_cols-1) + right) * 3;
	        bl_face_idx = (bottom * (num_cols-1) + left) * 3;
	        br_face_idx = (bottom * (num_cols-1) + right) * 3;

		normal.x = face_normals[tl_face_idx] +
			   face_normals[tr_face_idx] +
			   face_normals[bl_face_idx] +
			   face_normals[br_face_idx];

		normal.y = face_normals[tl_face_idx+1] +
			   face_normals[tr_face_idx+1] +
			   face_normals[bl_face_idx+1] +
			   face_normals[br_face_idx+1];

		normal.z = face_normals[tl_face_idx+2] +
			   face_normals[tr_face_idx+2] +
			   face_normals[bl_face_idx+2] +
			   face_normals[br_face_idx+2];

		normal.normalize ();

		result[face_idx+6] = (float)normal.x;
		result[face_idx+7] = (float)normal.y;
		result[face_idx+8] = (float)normal.z;

		// top right 
	        top = (v==0)? v : v-1;
		bottom = v;
		left = u;
		right = (u>=num_cols-2)? u : u+1;
	        tl_face_idx = (top * (num_cols-1) + left) * 3;
	        tr_face_idx = (top * (num_cols-1) + right) * 3;
	        bl_face_idx = (bottom * (num_cols-1) + left) * 3;
	        br_face_idx = (bottom * (num_cols-1) + right) * 3;

		normal.x = face_normals[tl_face_idx] +
			   face_normals[tr_face_idx] +
			   face_normals[bl_face_idx] +
			   face_normals[br_face_idx];

		normal.y = face_normals[tl_face_idx+1] +
			   face_normals[tr_face_idx+1] +
			   face_normals[bl_face_idx+1] +
			   face_normals[br_face_idx+1];

		normal.z = face_normals[tl_face_idx+2] +
			   face_normals[tr_face_idx+2] +
			   face_normals[bl_face_idx+2] +
			   face_normals[br_face_idx+2];

		normal.normalize ();

		result[face_idx+9] = (float)normal.x;
		result[face_idx+10] = (float)normal.y;
		result[face_idx+11] = (float)normal.z;
            }
        }
	return result;
    }
}
